package de.mmis.core.tuplespace;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeoutException;

import org.apache.log4j.Logger;

import de.mmis.core.base.Position;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.gclass.GParameterized;
import de.mmis.core.base.informationprovider.DeviceInformationProvider;
import de.mmis.core.base.informationprovider.PropagatingDeviceInformationProvider;
import de.mmis.core.pojop.NetworkProxy;

/**
 * {@link DeviceInformationProvider} implementation that uses a
 * {@link TuplespaceInformationProvider} to receive data
 * 
 * @author Christian Koch
 * 
 */
public class TuplespacePublisherInformationProvider extends
		PropagatingDeviceInformationProvider {

	private final static Logger LOGGER = Logger
			.getLogger(TuplespacePublisherInformationProvider.class);

	private static final Tree MAP_ID = new LeafNode("device-information");
	protected Tuplespace space;
	protected TuplespaceMultikeyMap map;

	private static final String MAP_NAME = "informationprovider";

	private final ClassLoader classLoader;

	// use lazy initialization as there may be no calls propagated to this
	// information provider anyway
	private boolean initialized;

	private String host;
	private int port;

	/**
	 * create new instance with information published on specified
	 * {@link Tuplespace}
	 * 
	 * @param host
	 *            host name or IP of the {@link Tuplespace} to connect to
	 * @param port
	 *            port number of the {@link Tuplespace} to connect to
	 * @param initialize
	 *            if set to true, the connection to {@link Tuplespace} will be
	 *            started immediately (otherwise it would be started while the
	 *            first method call is processed)
	 * @throws TuplespaceException
	 *             an error occurred while connecting or initializing data
	 *             container
	 */
	public TuplespacePublisherInformationProvider(String host, int port,
			boolean initialize) throws TuplespaceException {
		this(host, port, initialize,
				TuplespacePublisherInformationProvider.class.getClassLoader(),
				null);
	}

	public TuplespacePublisherInformationProvider(Tuplespace space,
			boolean initialize) throws TuplespaceException {
		this(space, initialize, TuplespacePublisherInformationProvider.class
				.getClassLoader());
	}

	public TuplespacePublisherInformationProvider(String host, int port,
			boolean initialize, ClassLoader classLoader)
			throws TuplespaceException {
		this(host, port, initialize, classLoader, null);
	}

	public TuplespacePublisherInformationProvider(Tuplespace space,
			boolean initialize, ClassLoader classLoader)
			throws TuplespaceException {
		super(null);
		this.space = space;
		this.classLoader = classLoader;

		if (initialize)
			initialize();
	}

	/**
	 * create new instance with information published on specified
	 * {@link Tuplespace}
	 * 
	 * @param host
	 *            host name or IP of the {@link Tuplespace} to connect to
	 * @param port
	 *            port number of the {@link Tuplespace} to connect to
	 * @param initialize
	 *            if set to true, the connection to {@link Tuplespace} will be
	 *            started immediately (otherwise it would be started while the
	 *            first method call is processed)
	 * @param parent
	 *            parent information provider requests are propagated to if no
	 *            answer can be found. May be <code>null</code>.
	 * @throws TuplespaceException
	 *             an error occurred while connecting or initializing data
	 *             container
	 */
	public TuplespacePublisherInformationProvider(String host, int port,
			boolean initialize, ClassLoader classLoader,
			DeviceInformationProvider parent) throws TuplespaceException {
		super(parent);
		initialized = false;
		this.host = host;
		this.port = port;
		this.classLoader = classLoader;
		this.space = null;

		if (initialize)
			initialize();
	}

	private void initialize() throws TuplespaceException {
		if (space == null)
			try {
				space = NetworkProxy.createProxy(Tuplespace.class, host, port);
			} catch (IOException e) {
				throw new TuplespaceException(
						"error while creating NetworkProxy", e);
			}

		map = new TuplespaceMultikeyMap(space, MAP_NAME, MAP_ID);

		if (!map.isPresent()) {
			LOGGER.warn("information provider map \"" + MAP_ID
					+ "\" not found, generating new one");
			map.initMap();
		}
		initialized = true;
	}

	@Override
	public Map<String, Object> getPropertiesMap(String id) {
		try {
			if (!initialized)
				initialize();

			Map<Tree, Tree> m = map.getKeyValueMap(Tree.fromObject(id));

			Map<String, Object> ret = new HashMap<String, Object>();
			for (Entry<Tree, Tree> e : m.entrySet())
				ret.put(e.getKey().getAsString(),
						e.getValue().getAs(Object.class));
			return ret;
		} catch (TuplespaceException e) {
			LOGGER.error("error while getting properties map", e);
		} catch (TimeoutException e) {
			LOGGER.error("error while getting properties map", e);
		} catch (DeserializableException e) {
			LOGGER.error("error while getting properties map", e);
		}

		return super.getPropertiesMap(id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * de.mmis.informationprovider.LabGUIInformationProvider#getDisplayName(
	 * java.lang.String)
	 */
	@Override
	public Map<String, String> getDisplayName(String id) {
		try {
			if (!initialized)
				initialize();

			Tree exp = map.getValue(Tree.fromObject(id),
					Tree.fromObject("display-name"));
			if (exp != null)
				return exp.getAs(new GParameterized(Map.class, new GClass[] {
						GClass.fromClass(String.class),
						GClass.fromClass(String.class) }));
		} catch (TuplespaceException e) {
			LOGGER.error("error while getting display name", e);
		} catch (TimeoutException e) {
			LOGGER.error("error while getting display name", e);
		} catch (DeserializableException e) {
			LOGGER.error("error while getting display name", e);
		}

		return super.getDisplayName(id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * de.mmis.informationprovider.LabGUIInformationProvider#getPosition(java
	 * .lang.String)
	 */
	@Override
	public Map<String, Position> getPosition(String id) {
		try {
			if (!initialized)
				initialize();

			Tree exp = map.getValue(Tree.fromObject(id),
					Tree.fromObject("position"));
			if (exp != null)
				return exp.getAs(new GParameterized(Map.class, new GClass[] {
						GClass.fromClass(String.class),
						GClass.fromClass(Position.class) }));
		} catch (TuplespaceException e) {
			LOGGER.error("error while getting position", e);
		} catch (TimeoutException e) {
			LOGGER.error("error while getting position", e);
		} catch (DeserializableException e) {
			LOGGER.error("error while getting position", e);
		}
		return super.getPosition(id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.mmis.informationprovider.LabGUIInformationProvider#
	 * getLocalInstanceSExpression(java.lang.String)
	 */
	@Override
	public Tree getLocalInstanceTree(String id) {
		try {
			if (!initialized)
				initialize();

			return map.getValue(Tree.fromObject(id),
					Tree.fromObject("local-instance"));
		} catch (TuplespaceException e) {
			LOGGER.error("error while getting local instance tree", e);
		} catch (TimeoutException e) {
			LOGGER.error("error while getting local instance tree", e);
		}
		return super.getLocalInstanceTree(id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * de.mmis.informationprovider.DeviceInformationProvider#getDeviceClassName
	 * (java.lang.String)
	 */
	@Override
	public de.mmis.core.base.Type getDeviceType(String id) {
		Tree exp;
		try {
			if (!initialized)
				initialize();

			exp = space.readIfExists(new InnerNode(new LeafNode(":device"),
					new LeafNode("?"), Tree.fromObject(id)));
		} catch (TuplespaceException e) {
			LOGGER.error("ERROR while determining device type name:", e);
			return super.getDeviceType(id);
		}
		if (exp == null) {
			LOGGER.warn("device type name in tuplespace has wrong format: "
					+ exp + "; ignoring");
			return super.getDeviceType(id);
		}
		try {
			return ((InnerNode) exp).getSubTree(1).getAs(
					de.mmis.core.base.Type.class, classLoader);
		} catch (DeserializableException e) {
			LOGGER.error("error while getting local instance tree", e);
		}
		return super.getDeviceType(id);
	}

	@Override
	public de.mmis.core.base.Type[] getInterfaces(
			de.mmis.core.base.Type deviceType) {
		ArrayList<Tree> exps;
		try {
			if (!initialized)
				initialize();

			exps = space.scan(new InnerNode(Tree.fromObject(deviceType),
					new LeafNode("implements"), new LeafNode("?")));
		} catch (TuplespaceException e) {
			LOGGER.error("ERROR while determining device class interfaces:", e);
			return super.getInterfaces(deviceType);
		}
		ArrayList<de.mmis.core.base.Type> ret = new ArrayList<de.mmis.core.base.Type>();
		for (Tree exp : exps) {
			try {
				ret.add(((InnerNode) exp).getSubTree(2).getAs(
						de.mmis.core.base.Type.class, classLoader));
			} catch (DeserializableException e) {
				LOGGER.error("error while getting device interfaces", e);
			}
		}
		return ret.toArray(new de.mmis.core.base.Type[ret.size()]);
	}
}
